﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Text;
using System.Web;
using System.Xml;
using RemoteTestRunner.Contracts;
using RemoteTestRunner.DataAccess;

namespace RemoteTestRunner.BusinessLogic
{
    public class RtrLib : MarshalByRefObject, IRtrService
    {

        public RtrLib(IRtrSettings rtrSettings)
        {
            _rtrSettings = rtrSettings;
        }

        private IRtrSettings _rtrSettings;

        #region IRTRService Properties

        private TestProjectAccess _projectsDataAccess;

        public TestProjectAccess ProjectsDataAccess
        {
            get { return _projectsDataAccess ?? (ProjectsDataAccess = new TestProjectAccess(_rtrSettings.DataPath)); }

            private set { _projectsDataAccess = value; }
        }

        #endregion

        #region Static Methods

        /// <summary>
        ///   Starts an e-mail message using the default e-mail client.
        /// </summary>
        /// <param name = "subject">The subject.</param>
        /// <param name = "messageBody">The message body.</param>
        public static void StartEmail(string subject, string messageBody)
        {
            Process.Start(
                string.Format("mailto:?subject={0}&body={1}", HttpUtility.UrlPathEncode(subject), messageBody));
        }

        /// <summary>
        ///   Starts an e-mail message using the default e-mail client.
        /// </summary>
        /// <param name = "testResult">An instance of a <see cref = "TestResult" /> objected
        ///   containing the details for the test run.</param>
        public static void StartEmail(TestResult testResult)
        {
            var subject = "Service Level Test Notification [{0}]";
            var message =
                "Test Run: {6}%0ATotal: {0}  Executed: {1}  Passed: {2}  Failed: {3}  Timed Out: {4}  Inconclusive: {5} %0A%0A";

            message = string.Format(
                message,
                testResult.TotalCount,
                testResult.TotalExecuted,
                testResult.TotalPassed,
                testResult.TotalFailed,
                testResult.TotalTimedOut,
                testResult.TotalInconclusive,
                testResult.TestRunName);

            foreach (var item in testResult.TestResults)
            {
                // Do not e-mail details on passed tests
                if (!item.Passed)
                {
                    message += item + " %0A%0A";
                }

                if (item.DataDrivenResults != null)
                {
                    int dataRow = 0;

                    foreach (var ddItem in item.DataDrivenResults)
                    {
                        dataRow++;

                        if (!ddItem.Passed)
                        {
                            message += string.Format("     ==> [row: {0}]", dataRow) +
                                       ddItem + " %0A%0A";
                        }
                    }
                }
            }

            if (testResult.Outcome == "Completed")
            {
                subject = string.Format(subject, "Passed");
            }
            else if (testResult.TotalCount > 0)
            {
                subject = string.Format(subject, "Failed");
            }
            else
            {
                subject = string.Format(subject, "No Tests Run");
            }

            StartEmail(subject, message);
        }

        #endregion

        #region Public Methods

        #region Delegates

        public delegate TestResultSummary RunTestDelegate(
            Project project, Test test, List<string> testsToRun, int timeoutSeconds);

        #endregion

        /// <summary>
        ///   Gets a list of configured projects
        /// </summary>
        /// <returns></returns>
        public List<Project> GetProjects()
        {
            return ProjectsDataAccess.GetProjects();
        }

        /// <summary>
        ///   Save the projects to the rtr project configuration file.
        /// </summary>
        /// <param name = "projects"></param>
        public void SaveProjects(List<Project> projects)
        {
            ProjectsDataAccess.SaveProjects(projects);
        }

        /// <summary>
        ///   Gets the specified test row.
        /// </summary>
        /// <param name = "testId">The ID of the test to return.</param>
        /// <returns>A <see cref = "DataRow" /> containing the specified test information.</returns>
        public TestInfo GetTestRow(int testId)
        {
            var row = ProjectsDataAccess.GetTestRow(testId);

            if (row != null)
            {
                var result = new TestInfo
                                 {
                                     Id = (int)row["ID"],
                                     ProjectId = (int)row["ProjectID"],
                                     Name = row["Name"].ToString(),
                                     AssemblyPath = row["AssemblyPath"].ToString(),
                                     ConfigPath = row["ConfigPath"].ToString(),
                                     CreatedBy = row["CreatedBy"].ToString(),
                                     CreatedDate = DateTime.Parse(row["CreatedDate"].ToString()),
                                     ModifiedBy = row["ModifiedBy"].ToString(),
                                     ModifiedDate = DateTime.Parse(row["ModifiedDate"].ToString()),
                                     RunConfigPath = row["RunConfigPath"].ToString()
                                 };

                return result;
            }

            return null;
        }

        /// <summary>
        ///   Gets a collection of data source information.
        /// </summary>
        /// <param name = "appConfigPath">The application configuration file to be parsed.</param>
        /// <returns>A list of <see cref = "Contracts.DataSourceInfo" /> containing information for each
        ///   data source found in the configuration file.</returns>
        public List<DataSourceInfo> GetDataSources(string appConfigPath)
        {
            var dataSources = new List<DataSourceInfo>();
            DataSourceInfo dsInfo;
            XmlDocument doc;
            XmlNode dsNameNode;
            XmlNode dsBaseUriNode;

            if (File.Exists(appConfigPath))
            {
                doc = new XmlDocument();
                doc.Load(appConfigPath);

                var dataSourceNodes = doc.SelectNodes("/configuration/microsoft.visualstudio.testtools/dataSources/add");

                if (dataSourceNodes != null)
                {
                    foreach (XmlNode dsNode in dataSourceNodes)
                    {
                        dsNameNode = doc.SelectSingleNode(
                            string.Format("/configuration/connectionStrings/add[@name='{0}']",
                                          dsNode.Attributes.GetNamedItem("connectionString").Value));

                        dsInfo = new DataSourceInfo
                                     {
                                         Name = Utilities.GetAttributeValue(dsNode.Attributes, "name"),
                                         DataSourcePath =
                                             Utilities.GetConnectionStringSetting(
                                                 Utilities.GetAttributeValue(dsNameNode.Attributes, "connectionString"),
                                                 ConnectionStringSetting.DataSource),
                                         ConnectionStringName =
                                             Utilities.GetAttributeValue(dsNode.Attributes, "connectionString"),
                                         DataTableName = Utilities.GetAttributeValue(dsNode.Attributes, "dataTableName"),
                                         DataAccessMethod =
                                             Utilities.GetAttributeValue(dsNode.Attributes, "dataAccessMethod")
                                     };

                        dsBaseUriNode = doc.SelectSingleNode(
                            string.Format(
                                "/configuration/appSettings/add[@key='{0}']",
                                "DDTCB:" + dsInfo.ConnectionStringName));

                        if (dsBaseUriNode != null)
                        {
                            dsInfo.DataSourceBaseUri = Utilities.GetAttributeValue(dsBaseUriNode.Attributes, "value");
                        }

                        dataSources.Add(dsInfo);
                    }
                }
            }

            return dataSources;
        }

        /// <summary>
        ///   Runs the test for the test ID passed in.
        /// </summary>
        /// <param name = "project"></param>
        /// <param name = "test">The ID of the test to be ran.</param>
        /// <param name = "testsToRun">A list of the test to run.  The expected format is 'Class.Method'.</param>
        /// <param name = "timeoutSeconds"></param>
        /// <returns>
        ///   The path of the generated test results file.
        /// </returns>
        public TestResultSummary RunTest(Project project, Test test, List<string> testsToRun, int timeoutSeconds)
        {
            Debug.Assert(File.Exists(_rtrSettings.MsTestPath), "MSTest.exe cannot be found at " + _rtrSettings.MsTestPath);

            List<DataSourceInfo> dataSources;
            TestResult testRunResults;
            TestResultSummary testRunResultsSummary;

            // Step 1: Retrieve test details
            //var test = GetTestRow(testId);

            // Step 2: Create a sub-folder within the working folder (for this test run)
            //var project = DataAccess.GetProject(test.ProjectId);
            //var projectName = project["Name"].ToString();
            var testFolder = Path.Combine(
                _rtrSettings.WorkingFolder,
                string.Format("{0}.{1}.{2}", project.Name, test.Name, DateTime.Now.ToString("MM-dd-yyyy_HH-mm-ss.fff")));
            Directory.CreateDirectory(testFolder);

            var testContainerFileName = Path.Combine(testFolder, Path.GetFileName(test.AssemblyPath));
            var testRunConfigFileName = test.RunConfigPath;

            // Step 3: Copy the binaries (based on the assembly path) to the working folder
            CopyFiles(
                new DirectoryInfo(Path.GetDirectoryName(test.AssemblyPath)),
                new DirectoryInfo(testFolder));

            // Step 4: Copy the test version of the config file, if one is specified
            var testConfigFileName = testContainerFileName + ".config";
            if (!string.IsNullOrEmpty(test.ConfigPath))
            {
                if (File.Exists(test.ConfigPath))
                {
                    File.Copy(test.ConfigPath, testConfigFileName, true);
                }
                else
                {
                    throw new FileNotFoundException("The specified configuration file: " + test.ConfigPath +
                                                    " could not be found.");
                }
            }

            // Step 5: Copy the data file(s) for this test
            if (File.Exists(test.ConfigPath))
            {
                dataSources = GetDataSources(test.ConfigPath);

                if ((dataSources != null) && (dataSources.Count > 0))
                {
                    // Download each file
                    foreach (var dataSource in dataSources)
                    {
                        if (!string.IsNullOrEmpty(dataSource.DataSourceBaseUri))
                        {
                            DownloadFile(dataSource.DataSourceBaseUri, dataSource.DataSourcePath);
                        }
                    }
                }
            }

            // Step 6: Build command line for MSTest
            var filesToSave = new List<string>();
            var testResultsFilename = Path.Combine(testFolder, Path.GetFileName(testFolder) + ".trx");
            filesToSave.Add(testResultsFilename);

            var testsArgs = new StringBuilder();
            foreach (var tests in testsToRun)
            {
                testsArgs.Append(" /test:" + tests + " ");
            }

            if (!string.IsNullOrEmpty(testRunConfigFileName))
            {
                testsArgs.AppendFormat(" /runconfig:\"{0}\" ", testRunConfigFileName);
            }

            var msTestArgs = string.Format(
                "/nologo /testcontainer:\"{0}\" /resultsfile:\"{1}\" {2}",
                testContainerFileName,
                testResultsFilename,
                testsArgs);

            // Step 7: Run the test(s)
            var msTestCommandLine = _rtrSettings.MsTestPath + ' ' + msTestArgs;
            File.WriteAllText(Path.Combine(testFolder, "mstest_command_line.txt"), msTestCommandLine);

            var startInfo = new ProcessStartInfo(_rtrSettings.MsTestPath, msTestArgs) { WindowStyle = ProcessWindowStyle.Hidden };

            using (var testProcess = new Process())
            {
                StreamReader standardOutput = null;
                ;
                bool redirectOutput = false;

                if (redirectOutput)
                {
                    startInfo.RedirectStandardOutput = true;
                    startInfo.UseShellExecute = false;
                }

                testProcess.StartInfo = startInfo;
                testProcess.Start();

                if (redirectOutput)
                {
                    standardOutput = testProcess.StandardOutput;
                }

                if (testProcess != null)
                {
                    // Wait for the tests to complete before continuing - timeout after 10 minutes
                    if (testProcess.WaitForExit(timeoutSeconds * 1000))
                    {
                        // Step 8: Parse the test results
                        try
                        {
                            var parser = TrxParserFactory.GetParser(testResultsFilename);
                            testRunResultsSummary = parser.ParseTestResultsSummary(testResultsFilename);
                            testRunResults = parser.ParseTestResults(testResultsFilename);
                        }
                        catch (FileNotFoundException ex)
                        {
                            throw new FileNotFoundException(
                                "The test results file: " + testResultsFilename + " was not found.", ex);
                        }
                    }
                    else
                    {
                        try
                        {
                            testProcess.Kill();
                        }
                        // ReSharper disable EmptyGeneralCatchClause
                        catch (Exception)
                        // ReSharper restore EmptyGeneralCatchClause
                        {
                            // Ignore any exception that occurred while trying to kill the process.
                        }

                        throw new TimeoutException(
                            "The tests have timed out during execution.  The tests were not completed.");
                    }
                }
                else
                {
                    throw new InvalidOperationException(
                        "The test process could not be started.  Make sure the testing framework is installed.");
                }

                if (redirectOutput)
                {
                    Debug.Print(standardOutput.ReadToEnd());
                }
            }

            //#if DEBUG

            //#else
            // Step 8: Remove temporary files
            testRunResults.MultiAssertResults.ForEach(
                result => filesToSave.Add(result.FileName));

            // Copy the save files to a safe directory
            var safeDirectory = _rtrSettings.WorkingFolder;
            for (var i = 0; i < filesToSave.Count; i++)
            {
                var destinationFile = Path.Combine(safeDirectory, Path.GetFileName(filesToSave[i]));
                File.Copy(filesToSave[i], destinationFile);
                filesToSave[i] = destinationFile;
            }

            // Delete the working folder and all contents, and then recreate it
            Directory.Delete(testFolder, true);
            Directory.CreateDirectory(testFolder);

            // Move the save files back to the working folder
            for (var i = 0; i < filesToSave.Count; i++)
            {
                var destinationFile = Path.Combine(testFolder, Path.GetFileName(filesToSave[i]));
                File.Move(filesToSave[i], destinationFile);
                filesToSave[i] = destinationFile;
            }
            //#endif

            return testRunResultsSummary;
        }

        /// <summary>
        ///   Gets the test result history.
        /// </summary>
        /// <param name = "project">The project to return history for.</param>
        /// <param name = "test">The test to return history for.</param>
        /// <returns></returns>
        public List<TestResultSummary> GetTestResultHistory(Project project, Test test)
        {
            var searchPattern = project.Name + "." + test.Name + ".*.*";
            var testDirectories = Directory.GetDirectories(_rtrSettings.WorkingFolder, searchPattern);

            var results = new List<TestResultSummary>();

            foreach (var directory in testDirectories)
            {
                // Find the trx file in the results directory
                var testResultFiles = Directory.GetFiles(directory, "*.trx");

                // There should only be one trx file in the directory, so we choose the first item incase this isn't true
                if (testResultFiles.Length > 0)
                {
                    try
                    {
                        var parser = TrxParserFactory.GetParser(testResultFiles[0]);
                        results.Add(parser.ParseTestResultsSummary(testResultFiles[0]));
                    }
                    // ReSharper disable EmptyGeneralCatchClause
                    catch (Exception)
                    // ReSharper restore EmptyGeneralCatchClause
                    {
                        // we are just going to ignore any parse error that occurs.
                        // TODO: handle parse errors more intelligently
                    }
                }
            }

            return results;
        }

        public TestResult GetTestResult(string path)
        {
            var parser = TrxParserFactory.GetParser(path);
            return parser.ParseTestResults(path);
        }

        public void DeleteTestResultHistoryItems(List<TestResultSummary> historyItemsToDelete)
        {
            foreach (var testResult in historyItemsToDelete)
            {
                var testPath = Path.GetDirectoryName(testResult.Path);
                Directory.Delete(testPath, true);
            }
        }

        /// <summary>
        ///   Gets a two level heirarchy of Classes decorated with the TestClassAttribute and Methods decorated with the TestMethodAttribute.
        /// </summary>
        /// <param name = "test">The test to process.</param>
        /// <returns>A 2 level hierchial class/method list.</returns>
        public List<TestClass> GetTestMethods(Test test)
        {
            try
            {
                var retriever = TestAssemblyMetadataFactory.GetAssemblyMetadataRetriever(test);
                return retriever.GetTestMethods(test);
            }
            catch (Exception ex)
            {
                TestClass testClass = new TestClass();
                testClass.Name = ex.Message;
                List<TestClass> result = new List<TestClass>();
                result.Add(testClass);
                return result;
            }
        }

        #endregion

        #region Private Methods

        /// <summary>
        ///   Recursively copies all files and folders from the source folder to the target
        ///   folder.
        /// </summary>
        /// <param name = "source">The source folder.</param>
        /// <param name = "target">The target folder.</param>
        private static void CopyFiles(DirectoryInfo source, DirectoryInfo target)
        {
            // Check if the target directory exists, if not, create it.
            if (Directory.Exists(target.FullName) == false)
            {
                Directory.CreateDirectory(target.FullName);
            }

            // Copy each file into it’s new directory.
            foreach (var fi in source.GetFiles())
            {
                fi.CopyTo(Path.Combine(target.ToString(), fi.Name), true);
            }

            // Copy each subdirectory using recursion.
            foreach (var diSourceSubDir in source.GetDirectories())
            {
                var nextTargetSubDir =
                    target.CreateSubdirectory(diSourceSubDir.Name);
                CopyFiles(diSourceSubDir, nextTargetSubDir);
            }
        }

        /// <summary>
        ///   Downloads the specified file.
        /// </summary>
        /// <param name = "requestUriString">The URI of the file to be downloaded.</param>
        /// <param name = "desinationPath">The desination path.</param>
        /// <returns><c>true</c> if the download is successful; otherwise, <c>false</c>.</returns>
        private static void DownloadFile(string requestUriString, string desinationPath)
        {
            try
            {
                using (var client = new WebClient())
                {
                    client.UseDefaultCredentials = true;
                    client.DownloadFile(requestUriString, HttpUtility.UrlDecode(desinationPath));
                }
            }
            catch (Exception ex)
            {
                throw new Exception("Unable to download " + requestUriString + " to " + desinationPath + ".", ex);
            }

        }

        #endregion
    }
}